Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add mapK to transformers #1987

Closed
wants to merge 5 commits into from

Conversation

andyscott
Copy link
Contributor

@andyscott andyscott commented Oct 21, 2017

Adds mapK to the following:

  • cats.data.EitherK
  • cats.data.EitherT
  • cats.data.Func
  • cats.data.IdT
  • cats.data.IndexedReaderWriterStateT
  • cats.data.IndexedStateT
  • cats.data.Kleisli
  • cats.data.Nested
  • cats.data.OneAnd
  • cats.data.OptionT
  • cats.data.Tuple2K
  • cats.data.WriterT
  • cats.free.Coyoneda
  • cats.free.Free
  • cats.free.FreeT
  • cats.free.Yoneda

The following are marked deprecated in favor of mapK:

  • cats.data.Kleisli#transform
  • cats.free.Coyoneda#transform
  • cats.free.Free#compile
  • cats.free.Free.compile
  • cats.free.FreeT#hoist

The primary motivation for this is to allow easy (and consistent) access to the transformer's underlying effect. For example, given a computation OptionT[Task, Int], you may want to call some of the underlying methods provided by Task.

@LukaJCB
Copy link
Member

LukaJCB commented Oct 21, 2017

This is great! Thanks for this. :) You do have some conflicts though. I also left a couple of tiny comments.

@@ -234,4 +234,3 @@ private[data] trait EitherKComonad[F[_], G[_]] extends Comonad[EitherK[F, G, ?]]
def extract[A](p: EitherK[F, G, A]): A =
p.extract
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is by accident?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's Emacs doing some extra housekeeping from having the file open. I'll revert it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also-- you're fast! This PR was up for just a few minutes and you were already on top of it 😄.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haha, yeah, sorry for nitpicking so quickly 😄

@@ -18,8 +18,11 @@ final case class Kleisli[F[_], A, B](run: A => F[B]) { self =>
def map[C](f: B => C)(implicit F: Functor[F]): Kleisli[F, A, C] =
Kleisli(a => F.map(run(a))(f))

def mapF[N[_], C](f: F[B] => N[C]): Kleisli[N, A, C] =
Kleisli(run andThen f)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can keep this as a deprecated alias? :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't mean to remove it-- I had added mapF to every transformer but wound up splitting off mapK additions to a separate PR. This just an accidental carry over from the split!

@LukaJCB
Copy link
Member

LukaJCB commented Oct 21, 2017

Oh and can you also add it to FreeT? :)

@andyscott
Copy link
Contributor Author

andyscott commented Oct 21, 2017

FreeT already has mapK a la hoist. I'll add mapK and deprecate hoist. Additionally, I'll add mapK to the yoneda duals.

@codecov-io
Copy link

codecov-io commented Oct 21, 2017

Codecov Report

Merging #1987 into master will decrease coverage by 0.09%.
The diff coverage is 70.58%.

Impacted file tree graph

@@            Coverage Diff            @@
##           master    #1987     +/-   ##
=========================================
- Coverage   96.21%   96.12%   -0.1%     
=========================================
  Files         272      272             
  Lines        4627     4640     +13     
  Branches      115      117      +2     
=========================================
+ Hits         4452     4460      +8     
- Misses        175      180      +5
Impacted Files Coverage Δ
free/src/main/scala/cats/free/Yoneda.scala 80% <0%> (-20%) ⬇️
free/src/main/scala/cats/free/Coyoneda.scala 84.61% <0%> (-7.06%) ⬇️
core/src/main/scala/cats/data/WriterT.scala 94.25% <100%> (+0.06%) ⬆️
core/src/main/scala/cats/data/EitherT.scala 97.58% <100%> (+0.01%) ⬆️
core/src/main/scala/cats/data/IdT.scala 97.36% <100%> (+0.07%) ⬆️
...in/scala/cats/data/IndexedReaderWriterStateT.scala 100% <100%> (ø) ⬆️
core/src/main/scala/cats/data/OptionT.scala 100% <100%> (ø) ⬆️
core/src/main/scala/cats/data/IndexedStateT.scala 100% <100%> (ø) ⬆️
core/src/main/scala/cats/data/Kleisli.scala 98.64% <50%> (-1.36%) ⬇️
free/src/main/scala/cats/free/FreeT.scala 90.41% <75%> (-1.26%) ⬇️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 7b4814a...d623055. Read the comment docs.

@andyscott
Copy link
Contributor Author

I've marked a few items as deprecated. If we'd like to keep the deprecation annotations, I'll need to know the release this PR is scheduled for and update them accordingly.

@johnynek
Copy link
Contributor

Can we not have a typeclass here? Seems like we want the analog to Functor no? Any reason not to add with so many instances? The composition law is pretty obvious. Seems worth it to me.

@andyscott
Copy link
Contributor Author

Definitely. There has been related discussion in the past, starting with #1713.

I'd like to add a FunctorK in the same manner as @kailuowang's mainecoon: https://github.com/kailuowang/mainecoon/blob/master/core/src/main/scala/mainecoon/FunctorK.scala.

@johnynek
Copy link
Contributor

johnynek commented Oct 22, 2017 via email

@andyscott
Copy link
Contributor Author

andyscott commented Oct 22, 2017

https://github.com/andyscott/cats/pull/1/files has FunctorK and instances for OptionT and EitherT. If that looks good, I'll give the same treatment to the rest of the instances.

A few notes on my implementation:

For a given transformer, FunctorK is invariant with regards to all non-effect parameters and requires no additional evidence. At the value level, only one instance is needed-- so I use a cast to specialize for non-effect parameters. This can be switched to the traditional implementation if desired.

There's no way easy way to provide arbitrary FunctionK transformations, so the laws for FunctorK just ask for specific transformations to test. In Mainecoon, laws ask for arbitrary instances but elsewhere Gen.const based instances are provided. If we have to use constant instances, I would prefer to pass them in as parameters for clarity.

@kailuowang
Copy link
Contributor

kailuowang commented Oct 23, 2017

There are two contention points IRT the type class,

  1. what does such an abstraction buy us? In another word, is there a realworld scenario where someone would need a def something[A[_[_]]: FunctorK] ?
  2. should the type class be of A[_[_]] or A[_[_], _]? A[_[_]] does cover more types, but due to the limitation of Scala syntax, it has a shortcoming, that is you cannot fix the effect type F[_] inside of it and get a type constructor. A[F] will have to be a concrete type, it cannot be a type constructor. On the other hand, A[_[_], _] can still be a type constructor even if you fix the effect type, since you can write A[F, _]. This gives you the ability to lift a F[_] ~> G[_] to A[F, _] ~> A[G, _], which makes it more like a higher kinded endofunctor with a fmap on FunctionK, and possibly enable more interesting operations like possibly composeK, etc.

On the other hand, both definitions of this type classes ( A[_[_]] or A[_[_], _]) probably don't matter that much because, again, there is no real use case for the abstraction anyway, the only use case is for people to call mapK on it.
To me the main incentive of adding such a type class would be

  1. having a unified law testing code and
  2. some possibly beneficial utility functions in the type class such as liftK: F[_] ~> G[_] => A[F, _] ~> A[G, _] and composeK: (FunctorK[A[_[_], _]], FunctorK[B[_[_], _]]) => FunctorK[λ[(a[_], b) => A[B[a, _], b]]].

@andyscott what's your incentive for this to be in the same manner as in mainecoon? to support final tagless encoding or do you have some other use cases?

Update: In mainecoon, there is a use case for the FunctorK[A[_[_]] abstraction. I have auto derivation code like this
implicit def derive[A[_[_]]: FunctorK, F[_], G[_])(implicit af: A[F], fk: F ~> G): A[G] = af.mapK(fk)
With this given an algebra with FunctorK and an available interpreter, I can automatically derive an interpreter of any effect type as long as there is a FunctionK route from the available interpreter to this effect type.
This sort of auto derivation can only be achieved with a type class.

@LukaJCB
Copy link
Member

LukaJCB commented Oct 23, 2017

I agree with @kailuowang that it's not very useful in general as a typeclass atleast for cats-core. The only reason I could see, is that it might be useful for both cats-mtl and mainecoon which both depend on cats-core, so they don't have to define their own separately.

@andyscott
Copy link
Contributor Author

andyscott commented Oct 23, 2017 via email

@iravid
Copy link
Contributor

iravid commented Oct 23, 2017

Quick reminder - StateT won't have an instance; it imposes a F: Functor constraint due to the F[S => F[(S, A)]] encoding.

@kailuowang
Copy link
Contributor

@LukaJCB the need for a FunctorK in cats-mtl and mainecoon is actually different. In cats-mtl it took the shape of FunctorK[A[_[_], _]] until edmund decided it's not worth the abstraction, in mainecoon, it is required to have the shape of FunctorK[A[_[_]]], because that's the shape of final tagless encoded algebras.

@LukaJCB
Copy link
Member

LukaJCB commented Oct 23, 2017

Ah you're right! In that case, my vote goes to merging it as is right now :)
I can definitely empathize with @andyscott's situation, as I have wanted the exact same thing in the past 😄

@andyscott andyscott changed the title Add mapK to most (hopefully all) transformers Add mapK to transformers Oct 23, 2017
@andyscott
Copy link
Contributor Author

andyscott commented Oct 23, 2017

@iravid Yep. Everything except IRWST and IndexedStateT could have an instance if we add FunctorK.

@andyscott
Copy link
Contributor Author

I think all feedback has been addressed. Codecov has failed the PR because the deprecated methods don't have coverage (they simply forward to mapK).

@kailuowang
Copy link
Contributor

@andyscott it can also be added to

  • EitherK (on the right F)
  • Tuple2K (on the right F)
  • Free
  • Func
  • Nested
  • OneAnd
    Also I noticed that tests/src/test/scala/cats/tests/IndexedReaderWriterStateTTests.scala
    should've been renamed to tests/src/test/scala/cats/tests/IndexedReaderWriterStateTSuite.scala
    Let me know if you have time to add those , if not I'll be more than happy to submit a PR against your branch.

@johnynek shall we unify the methods first in this PR and determine the type class later. There have been several attempts to add this FunctorK type class, none of them reached the consensus. If anything that should tell us that we shouldn't rush it into cats.core 1.0.
On the other hand, these straightforward mapK methods have been needed and I don't see any harm of having them without the type class.

@andyscott
Copy link
Contributor Author

@kailuowang I'll take care of them within the next day.

@johnynek
Copy link
Contributor

I guess we can just copy this pattern all over.

I kind of hate that in a library about typeclasses we are ignoring this.

Also, I think it would compose in a nice way for the transformers. So, if you have OptionT[EitherT[F, A, B]] you could compose to point into something deeper.

Of course, the scala type system issues are a pain, so I guess if we don't have a clean way to write this typeclass we should punt.

@andyscott
Copy link
Contributor Author

andyscott commented Oct 24, 2017

There have been discussions on having a cats-more or extras module. While FunctorK doesn't provide a ton of utility to end users of cats, it would provide utility to other library authors (and anyone who wants to check the laws). Maybe the typeclass should go there?

see #1940

@johnynek
Copy link
Contributor

I'm actually -1 on tiny modules that we then have to bolt on a ton of boiler plate and imports.

FunctorK if we did it right would be a trivial increase in the size of cats. Having to add an additional import, build setup, implicit imports, etc... seems unmotivated to me.

The main motivations, in my view, for splitting modules are:

  1. decrease jar size when many don't need everything. This should be done when we can save megabytes, not kilobytes.
  2. reduce dependencies, so users who do not need a module don't complicate their build problem with additional dependencies.

I really don't think there are significant reasons to split other than that. This does not seem to be one of those two cases.

@andyscott
Copy link
Contributor Author

I'm also -1 on tiny modules for the same reasons. But if we can't add something to the core module and it's valuable, then having it in a tiny module is arguably better than not having it at all.

@LukaJCB
Copy link
Member

LukaJCB commented Oct 24, 2017

We should continue the discussion here #1713. And like @kailuowang said we could potentially still add it if we come to a good conclusion.

@kailuowang kailuowang added this to the 1.0.0 milestone Oct 27, 2017
@andyscott
Copy link
Contributor Author

andyscott commented Oct 29, 2017

I've added missing instances. The remaining tests still need to be added.

When adding the method to Free, I realized I have it set up so mapK on Free operates on the free functor S but mapK on FreeT operates on the transformer context M. Should mapK for FreeT also operate on the functor S?

λ[FunctionK[S, Free[T, ?]]](fa => Suspend(f(fa)))
}(Free.catsFreeMonadForFree)
*/
@deprecated("Use mapK", "1.0.0")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure if we want to deprecate this one. compile seems a meaningful alias.

@kailuowang
Copy link
Contributor

kailuowang commented Oct 30, 2017

I am fine withmapK operates on S on Free and M on FreeT .

Copy link
Contributor

@johnynek johnynek left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks good. One minor comment.

@@ -218,10 +231,17 @@ object Free extends FreeInstances {
pure(()).flatMap(_ => value)

/**
* a FunctionK, suitable for composition, which calls mapK
*/
def mapK[F[_], G[_]](fk: FunctionK[F, G]): FunctionK[Free[F, ?], Free[G, ?]] =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why FunctionK[F, G] here but F ~> G in most other places?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method is a copy-paste-rename from the original Free.compile. I'll go through and switch FunctionK to ~> in the files modified by this PR, for consistency.

@LukaJCB
Copy link
Member

LukaJCB commented Oct 31, 2017

The Travis build fails because the Parallel instance for Kleisli uses the transform method which is now deprecated, you might need to rebase and change that one :)
Otherwise LGTM! 🎉

@kailuowang
Copy link
Contributor

Actually can we also not deprecate hoist that's a common alias.

@LukaJCB LukaJCB mentioned this pull request Oct 31, 2017
@kailuowang
Copy link
Contributor

@andyscott we continued this in #2000 so that we can release this morning. Sorry for the rush. thanks for this awesome contribution.

@kailuowang
Copy link
Contributor

closing as #2000 is merged

@kailuowang kailuowang closed this Oct 31, 2017
@kailuowang kailuowang modified the milestones: 1.0.0, 1.0.0-RC1 Oct 31, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants